#
# Search.py
# (c) 2008 Will Baker.
#

import re

from SearchResult import SearchResult

class Search:
    term          = re.compile( "(?<=^)[a-z0-9]+")
    meta_term     = re.compile( "^/[a-z0-9]+")
    negative_term = re.compile( "^\![/]?[a-z0-9]+" )

    """
    A class through which a list of comics can be found.
    """
    def __init__( self, search_string ):
        """
        Constructor. Parses a search string into separate search tags.
        """
        self.terms = []
        self.meta_terms = []
        self.negative_terms = []
        
        terms = search_string.split( ' ' )
        for t in terms:
            self.terms.extend( Search.term.findall(t) )
            self.meta_terms.extend( Search.meta_term.findall(t) ) 
            self.negative_terms.extend( Search.negative_term.findall(t) )
                        
    def apply( self, library ):
        """
        Search a library using the search terms in this Search.
        """
        comics = set()
        tags = {}

        # Find all comics that match a term.
        for term in self.terms:
            for tag in library.tags.keys():
                if tag[0] != '/' and tag[0] != '!' and term in tag:
                    if not tag in tags:
                        tags[tag] = 0
                    tags[tag] += self.calculate_relevance( term, tag )
                    comics = comics.union( library.tags[tag] )
                    
        # Find all comics that match a metaterm.
        for term in self.meta_terms:
            for tag in library.tags.keys():
                if tag[0] == '/' and term in tag:
                    if not tag in tags:
                        tags[tag] = 0
                    tags[tag] += 0.1 * self.calculate_relevance( term, tag )
                    comics = comics.union( library.tags[tag] )
                    
        # Find all comics that match a negative term (and remove them from the set)
        for term in self.negative_terms:
            for tag in library.tags.keys():
                if term[1:] in tag:
                    comics = comics.difference( library.tags[tag] )
        
        # Calculate the relevance of each comic and create a SearchResult for it.
        results = []
        for c in comics:
            relevance = 0
            
            for t in tags.keys():
                if t in c.tags:
                    relevance += tags[t]
        
            results.append( SearchResult(c, relevance) )
            
        # Sort the results by relevance and by the most appropriate tag.
        sort_tags = lambda a, b: cmp( tags[a], tags[b] )
        sorted_tags = tags.keys()
        sorted_tags.sort( sort_tags )
        
        for s in sorted_tags:
            results.sort( SearchResult.sort_by_tag(s) )
        results.sort( SearchResult.sort_by_relevance )
        return results
                
    def calculate_relevance( self, term, tag ):
        """
        Calculates the relevance of a term with respect to a tag. The relevance
        is the number of characters in the term divided by the number of characters
        in the token (not the tag) that it matched. A token is simply a whitespace
        deliminated string in the tag.
        
        This function assumes that 'term' is in fact found within 'tag', otherwise
        a ValueError exception will be raised.
        """
        index = tag.index( term )
        
        s = index
        while s >= 0 and not tag[s].isspace():
            s -= 1
        
        e = index
        while e < len(tag) and not tag[e].isspace():
            e += 1
            
        return len(term) / float(e - s)
